#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
AtlasSplit — Stylish CLI (DeepSeek-first)
- Default backend: DeepSeek (base_url/model/api_key prefilled)
- Greeting on startup
- "settings" command to edit model/base_url/api_key (persisted in .atlasplit_cli.json)
- Interactive wizard and non-interactive run still supported
This CLI expects an `agent.py` exposing:
    run(excel_path, task, sheet="", preview_rows=30, retries=2, mode="SAFE") -> bool
"""
import os
import sys
import json
import importlib
from pathlib import Path
from typing import Optional

import typer
from rich.console import Console
from rich.panel import Panel
from rich.table import Table

from rich.align import Align
from rich import box
from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
import questionary
from rich.theme import Theme
from atlas_config import THEME, DEFAULTS, SAFE_MODES, TASK_TEMPLATES

# ===== Theme & App =====

console = Console(theme=THEME)
app = typer.Typer(add_completion=False, help="AtlasSplit — Spreadsheet Allocation & Settlement Agent")

BANNER = r"""
  █████╗ ████████╗██╗      █████╗ ███████╗
 ██╔══██╗╚══██╔══╝██║     ██╔══██╗██╔════╝
 ███████║   ██║   ██║     ███████║█████╗  
 ██╔══██║   ██║   ██║     ██╔══██║██╔══╝  
 ██║  ██║   ██║   ███████╗██║  ██║███████╗
 ╚═╝  ╚═╝   ╚═╝   ╚══════╝╚═╝  ╚═╝╚══════╝

Natural language → controlled code → local execution → auditable results
"""


CONFIG_PATH = Path(".atlasplit_cli.json")


def main_menu():
    """首页菜单：集中入口"""
    while True:
        cfg = load_config()
        choice = q_select(
            "选择一个功能",
            ["Run (quick)", "Wizard", "Chat", "Ops (file ops)", "Settings", "Exit"],
            default="Run (quick)"
        )

        if choice == "Run (quick)":
            # 最少输入就跑一次
            excel_path = questionary.text("Excel/CSV path:").ask()
            from pathlib import Path as _Path
            excel = _Path(excel_path).expanduser().resolve()
            if not excel.exists():
                console.print(f"[bad]File not found:[/bad] {excel}")
                continue

            sheet = questionary.text("Sheet name (optional):").ask()
            task = questionary.text(
                "Describe your task (自然语言):",
                default=(load_config().get("quick_task_default") or "")
            ).ask() or (load_config().get("quick_task_default") or "")
            mode = q_select("Execution mode:", SAFE_MODES, default=cfg.get("mode_default", "SAFE"))
            try:
                retries = int(questionary.text("Retry times on failure:", default="2").ask() or "2")
            except Exception:
                retries = 2

            final = set_env_from(cfg)  # 使用已保存的 base_url/model/api_key
            summarize_config(excel, sheet, task, mode, final["base_url"], final["model"])
            if not questionary.confirm("Proceed?", default=True).ask():
                continue

            agent = load_agent()
            ok = run_agent(agent, excel, task, sheet, retries, mode)
            console.print("\n[good]DONE[/good]" if ok else "\n[bad]FAILED[/bad]")
            # 回主菜单
            continue

        elif choice == "Wizard":
            wizard()
            # 向导里有自己的流程；回到主菜单
            continue

        elif choice == "Chat":
            # 直接调用我们定义的 chat() 函数（使用默认 system/temperature）
            chat()
            continue

        elif choice == "Ops (file ops)":
            prompt = questionary.text("描述要做什么（默认仅演示，不会修改）：").ask() or ""
            # 演示模式（dry-run），不强制，目录默认 "."
            ops(prompt=prompt, dirpath=".", apply=False, force=False)
            continue

        elif choice == "Settings":
            # 让用户同时设置默认模式
            base = questionary.text("Base URL:", default=cfg["base_url"]).ask()
            model = questionary.text("Model:", default=cfg["model"]).ask()
            api   = questionary.password("API Key (留空保留当前):").ask() or cfg["api_key"]
            mode_default = q_select("Default execution mode:", SAFE_MODES, default=cfg.get("mode_default", "SAFE"))
            cfg.update({"base_url": base, "model": model, "api_key": api, "mode_default": mode_default})
            save_config(cfg)
            console.print("[good]Settings saved.[/good]")
            greet(cfg)
            continue

        else:  # Exit
            break


# ===== Utilities =====
def banner() -> None:
    panel = Panel.fit(Align.center(BANNER), title="[primary]atlas[/]", border_style="primary", box=box.ROUNDED)
    console.print(panel)

def load_config() -> dict:
    if CONFIG_PATH.exists():
        try:
            return {**DEFAULTS, **json.loads(CONFIG_PATH.read_text(encoding="utf-8"))}
        except Exception:
            return DEFAULTS.copy()
    return DEFAULTS.copy()

def save_config(cfg: dict) -> None:
    CONFIG_PATH.write_text(json.dumps(cfg, ensure_ascii=False, indent=2), encoding="utf-8")

def mask_key(k: str) -> str:
    if not k: return ""
    if len(k) <= 8: return "*" * len(k)
    return k[:4] + "*" * (len(k)-8) + k[-4:]

def greet(cfg: dict) -> None:
    table = Table(box=box.SIMPLE_HEAVY, title="Welcome to AtlasSplit", title_style="accent")
    table.add_column("Key", style="muted", no_wrap=True)
    table.add_column("Value", style="primary")
    table.add_row("Backend", "DeepSeek (default)")
    table.add_row("Base URL", cfg["base_url"])
    table.add_row("Model", cfg["model"])
    table.add_row("API Key", mask_key(cfg["api_key"]))
    console.print(table)
    console.print("[muted]Tip: Use [accent]atlas settings[/accent] to change model/base_url/api_key. Secrets are stored locally in .atlasplit_cli.json.[/muted]\n")

import re

def _latest_run_dir(root: Path = Path("runs")) -> Optional[Path]:
    if not root.exists():
        return None
    # 目录名像 20251004-065137，按名字排序就够用；退化时也可用 mtime
    dirs = [p for p in root.iterdir() if p.is_dir()]
    if not dirs:
        return None
    return sorted(dirs, key=lambda p: p.name, reverse=True)[0]

def _read_done_or_error(run_dir: Optional[Path]) -> tuple[str, str]:
    """
    返回 (summary, kind):
      - kind = "done"  -> 提取 stdout.log 里的 DONE: ... 一行
      - kind = "error" -> 读取 stderr.log 最后若干行
      - 都没有则 ("", "")
    """
    if not run_dir:
        return "", ""
    stdout = run_dir / "stdout.log"
    stderr = run_dir / "stderr.log"

    if stdout.exists():
        try:
            text = stdout.read_text(encoding="utf-8", errors="ignore")
            m = re.search(r"^DONE:\s*(.+)$", text, flags=re.MULTILINE)
            if m:
                return m.group(0), "done"  # 包含 "DONE: ..." 原句
        except Exception:
            pass

    if stderr.exists():
        try:
            lines = stderr.read_text(encoding="utf-8", errors="ignore").strip().splitlines()
            tail = "\n".join(lines[-15:])  # 取最后 15 行
            if tail:
                return tail, "error"
        except Exception:
            pass
    return "", ""


def q_select(prompt, choices, default=None):
    try:
        return questionary.select(prompt, choices=choices, default=default).ask()
    except Exception:
        # 终端不兼容时退回文本输入
        txt = questionary.text(f"{prompt} (输入其中之一)", default=default or choices[0]).ask()
        return (txt or choices[0]).strip()


def load_agent() -> object:
    try:
        return importlib.import_module("agent")
    except Exception as e:
        console.print("\n[bad]Error:[/bad] Cannot import [bold]agent.py[/bold].", style="bad")
        console.print("Make sure agent.py is in the same directory and exposes:", style="muted")
        console.print("  run(excel_path, task, sheet=\"\", preview_rows=30, retries=2, mode=\"SAFE\") -> bool\n", style="muted")
        console.print(f"[bad]{e}[/bad]")
        sys.exit(1)

def set_env_from(cfg: dict, base_url: Optional[str]=None, api_key: Optional[str]=None, model: Optional[str]=None) -> dict:
    # Apply overrides then export to env for the current process
    final = {
        "base_url": base_url or cfg.get("base_url") or DEFAULTS["base_url"],
        "api_key":  api_key  or cfg.get("api_key")  or DEFAULTS["api_key"],
        "model":    model    or cfg.get("model")    or DEFAULTS["model"],
    }
    os.environ["OPENAI_BASE_URL"] = final["base_url"]
    os.environ["OPENAI_API_KEY"]  = final["api_key"]
    os.environ["LLM_MODEL"]       = final["model"]
    return final

def summarize_config(excel: Path, sheet: str, task: str, mode: str, base_url: str, model: str) -> None:
    table = Table(box=box.SIMPLE_HEAVY, title="Run Summary", title_style="accent")
    table.add_column("Key", style="muted", no_wrap=True)
    table.add_column("Value", style="primary")
    table.add_row("Excel", str(excel))
    table.add_row("Sheet", sheet or "(auto)")
    table.add_row("Mode", mode)
    table.add_row("Model", model or os.getenv("LLM_MODEL", "(env or default)"))
    table.add_row("Base URL", base_url or os.getenv("OPENAI_BASE_URL", "(env or default)"))
    table.add_row("Outputs", "./outputs/result.xlsx, ./outputs/report.md")
    console.print(table)

def run_agent(agent, excel: Path, task: str, sheet: str, retries: int, mode: str) -> bool:
    with Progress(
        SpinnerColumn(style="accent"),
        TextColumn("[accent]Running AtlasSplit…[/accent] {task.description}"),
        TimeElapsedColumn(),
        transient=True,
    ) as progress:
        t = progress.add_task("Executing", total=None)
        ok = False
        try:
            ok = bool(agent.run(str(excel), task, sheet=sheet, preview_rows=30, retries=retries, mode=mode))
        except Exception as e:
            console.print(f"[bad]Execution error:[/bad] {e}")
        finally:
            progress.remove_task(t)
    # 运行结束后，尝试读取最新一次运行的 DONE / 错误摘要并展示
    latest = _latest_run_dir(Path("runs"))
    summary, kind = _read_done_or_error(latest)
    if summary:
        title = "Summary" if kind == "done" else "Error (tail)"
        console.print(Panel.fit(summary, title=title, border_style="primary"))

    return ok

# ===== Typer root callback: greet once =====

@app.callback(invoke_without_command=True)
def _entry(ctx: typer.Context):
    greet_once()
    if ctx.invoked_subcommand is None:
        main_menu()
        raise typer.Exit(code=0)


# ===== Commands =====
@app.command()
def settings(
    base_url: Optional[str] = typer.Option(None, "--base-url", help="OpenAI-compatible base URL"),
    model: Optional[str]    = typer.Option(None, "--model", help="Model name"),
    api_key: Optional[str]  = typer.Option(None, "--api-key", help="API key"),
    quick_task: Optional[str] = typer.Option(None, "--quick-task", help="默认 Run (quick) 的自然语言描述"),
    interactive: bool       = typer.Option(True, "--interactive/--no-interactive", help="Prompt for values when not provided"),
) -> None:
    """View or update saved settings (persisted in .atlasplit_cli.json)."""
    cfg = load_config()
    if interactive:
        base_url = base_url or questionary.text("Base URL:", default=cfg["base_url"]).ask()
        model    = model    or questionary.text("Model:", default=cfg["model"]).ask()
        api_key  = api_key  or questionary.password("API Key (leave blank to keep):").ask() or cfg["api_key"]
        quick_task = quick_task or questionary.text(
            "Default Run(quick) task:",
            default=cfg.get("quick_task_default", "")
        ).ask()
    # apply overrides if provided
    if base_url: cfg["base_url"] = base_url
    if model:    cfg["model"]    = model
    if api_key:  cfg["api_key"]  = api_key
    if quick_task is not None: cfg["quick_task_default"] = quick_task
    save_config(cfg)
    console.print("[good]Settings saved.[/good]")
    greet(cfg)

@app.command()
def wizard() -> None:
    """Interactive guided run with DeepSeek defaults."""
    cfg = load_config()

    # Backend selection (default DeepSeek)
    backend = questionary.select(
        "Select inference backend:",
        choices=["DeepSeek", "Local (llama.cpp/vLLM)", "OpenAI", "Custom"],
        default="DeepSeek",
    ).ask()

    base_url = cfg["base_url"]
    api_key  = cfg["api_key"]
    model    = cfg["model"]

    if backend == "DeepSeek":
        base_url = "https://api.deepseek.com/v1"
        api_key = os.getenv("OPENAI_API_KEY") or cfg["api_key"] or "sk-"
        model = os.getenv("LLM_MODEL") or cfg["model"] or "deepseek-coder"
        # Let user adjust if they want
        if questionary.confirm("Edit DeepSeek settings?", default=False).ask():
            base_url = questionary.text("Base URL:", default=base_url).ask()
            model    = questionary.text("Model (deepseek-chat / deepseek-coder):", default=model).ask()
            api_key  = questionary.password("API Key:", default=api_key).ask()
    elif backend == "Local (llama.cpp/vLLM)":
        port = questionary.text("Local server port (default 8000):", default="8000").ask()
        base_url = f"http://127.0.0.1:{port}/v1"
        api_key = api_key or "sk-local"
        model = questionary.text("Model name (e.g., qwen2.5-coder-7b-instruct):",
                                 default=model or "qwen2.5-coder-7b-instruct").ask()
    elif backend == "OpenAI":
        base_url = "https://api.openai.com/v1"
        api_key = questionary.password("OpenAI API Key (starts with sk-)").ask()
        model = questionary.text("Model (e.g., gpt-4o-mini):", default="gpt-4o-mini").ask()
    else:  # Custom
        base_url = questionary.text("Base URL (must end with /v1):", default=base_url or "http://127.0.0.1:8000/v1").ask()
        api_key  = questionary.text("API Key (optional for local):", default=api_key or "sk-local").ask()
        model    = questionary.text("Model name:", default=model or "qwen2.5-coder-7b-instruct").ask()

    # File and task inputs
    excel_path = questionary.text("Excel/CSV path:").ask()
    from pathlib import Path as _Path
    excel = _Path(excel_path).expanduser().resolve()
    if not excel.exists():
        console.print(f"[bad]File not found:[/bad] {excel}")
        raise typer.Exit(code=1)

    sheet = questionary.text("Sheet name (optional):").ask()

    tmpl = questionary.select("Pick a task template (or Custom):", choices=list(TASK_TEMPLATES.keys())).ask()
    if tmpl == "Custom":
        task = questionary.text("Describe your task (natural language):").ask()
    else:
        task = TASK_TEMPLATES[tmpl]

    mode = q_select("Execution mode:", SAFE_MODES, default=cfg.get("mode_default", "SAFE"))

    retries = int(questionary.text("Retry times on failure (default 2):", default="2").ask())

    # Apply and persist config if backend is DeepSeek (user likely wants it saved)
    if backend == "DeepSeek":
        save_config({"base_url": base_url, "api_key": api_key, "model": model})

    # Confirm summary
    final = set_env_from(load_config(), base_url=base_url, api_key=api_key, model=model)
    summarize_config(excel, sheet, task, mode, final["base_url"], final["model"])
    if not questionary.confirm("Proceed?", default=True).ask():
        console.print("[warn]Cancelled.[/warn]")
        raise typer.Exit(code=0)

    agent = load_agent()
    ok = run_agent(agent, excel, task, sheet, retries, mode)

    if ok:
        console.print("\n[good]DONE[/good] — Result written to [bold]./outputs/result.xlsx[/bold] and report.md")
    else:
        console.print("\n[bad]FAILED[/bad] — Check logs in [bold]./runs/[/bold] and report.md if any")

@app.command()
def run(
    excel: Path = typer.Option(..., "--excel", "-e", exists=True, file_okay=True, dir_okay=False, help="Excel/CSV path"),
    task: str = typer.Option(..., "--task", "-t", help="Natural language task"),
    sheet: str = typer.Option("", "--sheet", "-s", help="Sheet name"),
    mode: str = typer.Option(None, "--mode", "-m", help="SAFE | RELAXED | UNSAFE"),
    retries: int = typer.Option(2, "--retries", "-r", help="Retry times on failure"),
    base_url: Optional[str] = typer.Option(None, "--base-url", help="OpenAI-compatible base URL (overrides saved/env)"),
    api_key: Optional[str]  = typer.Option(None, "--api-key", help="API key (overrides saved/env)"),
    model: Optional[str]    = typer.Option(None, "--model", help="Model name (overrides saved/env)"),
) -> None:
    """Non-interactive run with options (uses saved settings by default)."""

    cfg = load_config()
    if mode is None:
        mode = cfg.get("mode_default", "SAFE")
    final = set_env_from(cfg, base_url=base_url, api_key=api_key, model=model)
    summarize_config(excel, sheet, task, mode, final["base_url"], final["model"])

    agent = load_agent()
    ok = run_agent(agent, excel, task, sheet, retries, mode)

    if ok:
        console.print("\n[good]DONE[/good] — Result written to [bold]./outputs/result.xlsx[/bold] and report.md")
    else:
        console.print("\n[bad]FAILED[/bad] — Check logs in [bold]./runs/[/bold] and report.md if any")

# --- greet once helper ---
def greet_once() -> None:
    if os.environ.get("ATLAS_GREETED") == "1":
        return
    cfg = load_config()
    banner()
    console.print("[primary]👋 Hello! Ready to split some spreadsheets.[/primary]\n")
    greet(cfg)
    os.environ["ATLAS_GREETED"] = "1"

# ===== File ops helpers =====
import os, shutil
from pathlib import Path as _P
from datetime import datetime

def _fmt_size(n: int) -> str:
    for unit in ["B","KB","MB","GB","TB"]:
        if n < 1024: return f"{n:.0f}{unit}"
        n /= 1024
    return f"{n:.0f}PB"

def _list_sorted(root: _P, by: str = "mtime", order: str = "asc"):
    items = []
    for p in root.iterdir():
        try:
            st = p.stat()
            items.append({
                "name": p.name,
                "is_dir": p.is_dir(),
                "size": st.st_size,
                "mtime": st.st_mtime,
            })
        except Exception:
            continue
    reverse = (order.lower() == "desc")
    key = {"mtime": "mtime", "name": "name", "size": "size"}.get(by, "mtime")
    items.sort(key=lambda x: x[key], reverse=reverse)
    table = Table(box=box.SIMPLE_HEAVY, title=f"List ({by}, {order})", title_style="accent")
    table.add_column("Name", style="primary")
    table.add_column("Type", style="muted")
    table.add_column("Size", style="muted", justify="right")
    table.add_column("Modified", style="muted")
    for it in items:
        ts = datetime.fromtimestamp(it["mtime"]).strftime("%Y-%m-%d %H:%M:%S")
        table.add_row(it["name"], "DIR" if it["is_dir"] else "FILE",
                      _fmt_size(it["size"]), ts)
    console.print(table)

def _mkdir_safe(root: _P, name: str, dry_run: bool):
    target = (root / name).resolve()
    if not str(target).startswith(str(root.resolve())):
        raise RuntimeError("Blocked: out-of-root path")
    if dry_run:
        console.print(f"[warn]DRY-RUN[/warn] would create: {target}")
    else:
        target.mkdir(parents=True, exist_ok=True)
        console.print(f"[good]Created[/good]: {target}")

def _empty_dir_safe(root: _P, dry_run: bool, protect: list[str] | None = None):
    """Move everything in root to trash/backup; never delete irreversibly."""
    protect = set(protect or [])
    entries = [p for p in root.iterdir() if p.name not in protect]
    if not entries:
        console.print("[warn]Nothing to clean.[/warn]")
        return
    if dry_run:
        console.print(f"[warn]DRY-RUN[/warn] would remove {len(entries)} items:")
        for p in entries[:20]:
            console.print(" -", p.name)
        if len(entries) > 20: console.print(f"... (+{len(entries)-20} more)")
        return
    # try send2trash first
    try:
        from send2trash import send2trash
        for p in entries:
            send2trash(str(p))
        console.print(f"[good]Moved to system trash[/good]: {len(entries)} items")
    except Exception:
        trash = root / (".trash_" + datetime.now().strftime("%Y%m%d-%H%M%S"))
        trash.mkdir(exist_ok=True)
        for p in entries:
            shutil.move(str(p), str(trash / p.name))
        console.print(f"[good]Moved to[/good] [accent]{trash}[/accent]: {len(entries)} items")

def _parse_ops_intent(prompt: str) -> dict:
    s = prompt.strip().lower()
    p = prompt.strip()

    # 1) 识别「创建xxx文件夹 / 新建xxx目录」
    import re
    m = re.search(r'创建\s*([^\s/\\：:，,。]+?)\s*(?:文件夹|目录)$', p)
    if not m:
        m = re.search(r'新建\s*([^\s/\\：:，,。]+?)\s*(?:文件夹|目录)$', p)
    if m:
        name = m.group(1).strip()
        return {"op": "mkdir", "name": name}

    # 2) 其他已有规则……
    # 排序查看
    if ("排序" in p) or ("按" in p and ("日期" in p or "时间" in p or "名称" in p or "大小" in p)):
        by = "mtime" if ("日期" in p or "时间" in p) else ("size" if "大小" in p else "name")
        order = "desc" if ("倒序" in p or "从新到旧" in p or "从后到前" in p or "desc" in s) else "asc"
        return {"op": "list_sorted", "by": by, "order": order}

    # 创建文件夹（原有兜底）
    if ("创建文件夹" in p) or ("新建文件夹" in p) or ("创建目录" in p) or ("新建目录" in p) or ("mkdir" in s):
        name = "new_folder"
        toks = p.replace("："," ").replace(":"," ").split()
        if toks: name = toks[-1].strip("，。.;\"'")
        return {"op": "mkdir", "name": name or "new_folder"}

    # 清空当前
    if (("清空" in p) or ("解散" in p)) and (("当前" in p) or ("本目录" in p) or ("当前文件夹" in p)):
        return {"op": "empty_dir"}

    # 兜底
    return {"op": "list_sorted", "by": "name", "order": "asc"}
# ===== Chat with model =====
@app.command()
def chat(
    system: str = typer.Option("You are a helpful assistant.", "--system", help="System prompt"),
    temperature: float = typer.Option(0.3, "--temperature", "-T", help="Sampling temperature"),
) -> None:
    """Chat with the current model (uses saved DeepSeek/OpenAI-compatible settings)."""
    cfg = load_config()
    final = set_env_from(cfg)  # export to env and get current config
    try:
        from openai import OpenAI
        client = OpenAI(base_url=final["base_url"], api_key=final["api_key"])
    except Exception as e:
        console.print(f"[bad]OpenAI client not available:[/bad] {e}")
        raise typer.Exit(code=1)

    console.print(Panel.fit("进入聊天模式：输入 /exit 退出。", title="chat", border_style="accent"))
    msgs = [{"role": "system", "content": system}]
    while True:
        try:
            user = questionary.text("you>").ask()
        except KeyboardInterrupt:
            break
        if not user or user.strip() in {"/exit", ":q", "quit"}:
            break
        msgs.append({"role": "user", "content": user})
        try:
            r = client.chat.completions.create(
                model=final["model"],
                messages=msgs,
                temperature=temperature,
                max_tokens=800,
            )
            ans = (r.choices[0].message.content or "").strip()
        except Exception as e:
            ans = f"[请求失败] {e}"
        console.print(Panel.fit(ans, title="model", border_style="primary"))
        msgs.append({"role": "assistant", "content": ans})

# ===== Natural-language file ops (SAFE) =====
@app.command()
def ops(
    prompt: str = typer.Option("", "--prompt", "-p", help="中文描述操作，如：按文件日期先后将文件排序 / 创建文件夹 data_2025 / 清空当前文件夹"),
    dirpath: str = typer.Option(".", "--dir", "-d", help="目标目录（默认当前目录）"),
    apply: bool = typer.Option(False, "--apply/--dry-run", help="执行或仅演示（默认 dry-run）"),
    force: bool = typer.Option(False, "--force/--no-force", help="危险操作需 --force 确认"),
) -> None:
    root = _P(dirpath).expanduser().resolve()
    if not root.exists() or not root.is_dir():
        console.print(f"[bad]Not a directory:[/bad] {root}")
        raise typer.Exit(code=1)
    if not prompt:
        prompt = questionary.text("描述要做什么：").ask() or ""

    cfg = load_config()  # ← 新增：读取配置
    plan = _parse_ops_intent(prompt)
    console.print(Panel.fit(json.dumps(plan, ensure_ascii=False, indent=2),
                            title="Plan", border_style="accent"))

    # ★ 自动 apply：仅对低风险 mkdir 生效
    if (plan.get("op") == "mkdir") and (not apply) and cfg.get("ops_auto_apply_mkdir", True):
        apply = True

    protected_names = {".git", ".venv", "runs", "outputs", "rag", "docker", "scripts"}

    if plan["op"] == "list_sorted":
        _list_sorted(root, by=plan.get("by", "mtime"), order=plan.get("order", "asc"))
        return

    if plan["op"] == "mkdir":
        name = plan.get("name", "new_folder")
        _mkdir_safe(root, name, dry_run=not apply)
        return

    if plan["op"] == "empty_dir":
        if not force:
            console.print("[bad]危险操作：将清空目录内容。请加 --force 确认。[/bad]")
            return
        _empty_dir_safe(root, dry_run=not apply, protect=list(protected_names))
        return

    console.print("[warn]未识别操作，已按按名称升序列出。[/warn]")
    _list_sorted(root, by="name", order="asc")


if __name__ == "__main__":
    app(prog_name="atlas")
